//Programa principal del juego


#include "Anilla.hpp"
#include "Modelo.hpp"
#include "Nave.hpp"
#include "Objetivo.hpp"
#include <sstream>
#include <stdio.h>
#include <stdlib.h>
#include "Terreno.hpp"
#include "Edificacion.hpp"
#include <AL/alut.h>
#include <AL/al.h>

#pragma GCC diagnostic ignored "-Wdeprecated-declarations"

#define MAX_ANILLAS 5 //cantidad maxima de anillos por escena
#define LIM_X 23 //limite x de la pantalla
#define LIM_Y 12 //limite y de la pantalla
#define MAX_OBJETIVOS 3 //cantidad maxima de objetivos por escena
#define MAX_EDIFICIOS 8 // cantidad de edificios
#define BUFFSIZE 1024 //tamano de buffer de picking
#define LIM_Z -50 //limite en Z


using namespace std;
GLuint texturaFondo; //id textura del fondo
int iwidth, iheight; //dimensiones textura
unsigned char* image = NULL; //buffer de imagen
GLuint selectBuf[BUFFSIZE]; //buffer de picking
GLint hits; //hits de picking
bool paused = false; // indica si el juego se encuentra en pausa
bool infinito = false;
//posicion de listeners y fuentes de sonido
ALfloat listenerPos[]={0.0,0.0,4.0};
ALfloat listenerVel[]={0.0,0.0,0.0};
ALfloat	listenerOri[]={0.0,0.0,1.0, 0.0,1.0,0.0};
ALfloat source0Pos[]={ 0.0, 0.0, -10.0};
ALfloat source0Vel[]={ 0.0, 0.0, 0.0};
//buffers de sonido
ALuint	buffer[NUM_BUFFERS];
ALuint	source[NUM_FUENTES];
ALuint  environment[NUM_ENVIRONMENTS];
ALsizei size,freq; 
ALenum 	format;
ALvoid 	*data;

float alto = 600; // alto incial de la ventana en pixels
float ancho = 800; // ancho inicial de la ventana en pixels
float aspectratio = (float) ancho / (float) alto; //variable auxiliar
char shipModel[] = "caso1.obj"; // nombre del modelo de la nave
char asteroidModel[] = "crystal_1.obj"; // nombre del modelo del objetivo
char texName[] = "tex_glaciar.ppm"; //textura del suelo
char texEdif[] = "tex_electro.ppm"; //textura edificios
char fondoName[] = "city.ppm"; //textura fondo
int frame = 0; //numero de frames que han pasado
long realTime = 0; // numero de milisegundos reales que han pasado
long virtualTime = 0; // indica cuando deberia ocurrir el proximo update
long fpsCheck = 0; //indica cuando debe actualizar el fps
long fpsCount = 30; //indica cuantos frames han pasado desde el ultimo check.
int fps = 30; //indica el ultimo fps calculado
int speed = 3; //milisegundos por frame (0 is fast, 6 is slow)
int speeds[][2] = {
    {8, 30}, //8 frames por update,  1 update cada 30 milisegundos
    {4, 30}, //4 frames por update,  1 update cada 30 milisegundos
    {2, 30}, //2 frames por update,  1 update cada 30 milisegundos
    {1, 30}, //1 frame por update,  1 update cada 30 milisegundos
    {1, 60}, //1 frame por update,  1 update cada 60 milisegundos
    {1, 120}, //1 frame por update,  1 update cada 120 milisegundos
    {1, 360} //1 frame por update,  1 update cada 360 milisegundos
};

bool endgame = false; //indica si el juego ha acabado.

//materiales del suelo
GLfloat mat_difuso[] = {0.8, 0.8, 1.0, 1.0};
GLfloat mat_ambiental[] = {0.0, 0.0, 0.0, 1.0};

GLfloat mat_especular[] = {0.5, 0.5, 0.5, 1.0};
GLfloat mat_emision[] = {1.0, 1.0, 1.0, 1.0};


Nave *nave = new Nave(2.0, shipModel, MAX_OBJETIVOS); // el jugador
Terreno *terreno = new Terreno(texName, 30, -1000, 1500); //suelo del juego
//Edificacion *edificio = new Edificacion(texEdif, nave, 0.0);
// lista los objetivos dibujados
Objetivo *objetivos[MAX_OBJETIVOS];
// lista las anillas dibujadas
Anilla *anillas[MAX_ANILLAS];

// lista de edificios
Edificacion * edificios[MAX_EDIFICIOS];

void initSonido(){
    
    alutInit(0,NULL);
    
    
    
    alListenerfv(AL_POSITION,listenerPos);
    alListenerfv(AL_VELOCITY,listenerVel);
    alListenerfv(AL_ORIENTATION,listenerOri);
    
    alGetError(); // clear any error messages
    
    if(alGetError() != AL_NO_ERROR) 
    {
        cout<<"- Error configurando fuentes y listeners!!"<<endl;
        exit(1);
    }
    else
    {
        cout<<"Fuentes y Listenes configurados"<<endl;

    }
    
    // 
    alGenBuffers(NUM_BUFFERS, buffer);

    ALboolean  al_bool;
    
   //cargando musica de fondo
    alutLoadWAVFile((ALbyte *) "armada.wav", &format, &data, &size,&freq, &al_bool);
    alBufferData(buffer[0],format,data,size,freq);
    alutUnloadWAV(format,data,size,freq);
    
     alGetError(); 
    alGenSources(NUM_FUENTES, source);

    if(alGetError() != AL_NO_ERROR) 
    {
        cout<<"- Error creando musica de fondo !!"<<endl;
        exit(2);
    }
    else
    {
        cout<<"Musica de Fondo generada"<<endl;
    }

    //configurar musica de fondo
    alSourcef(source[0],AL_PITCH,1.0f);
    alSourcef(source[0],AL_GAIN,1.0f);
    alSourcefv(source[0],AL_POSITION,source0Pos);
    alSourcefv(source[0],AL_VELOCITY,source0Vel);
    alSourcei(source[0],AL_BUFFER,buffer[0]);
    alSourcei(source[0],AL_LOOPING,AL_TRUE);
    alSourcePlay(source[0]);
    
    

}

//iniciar proceso de picking
void startPicking(int x, int y) {

    GLint viewport[4];
    float ratio;

    glSelectBuffer(BUFFSIZE, selectBuf);

    glGetIntegerv(GL_VIEWPORT, viewport);

    glRenderMode(GL_SELECT);

    glInitNames();


    glMatrixMode(GL_PROJECTION);
    glPushMatrix();
    glLoadIdentity();

    gluPickMatrix(x, viewport[3] - y, 1.0, 1.0, viewport);
    
    gluPerspective(60, aspectratio, 0.1, 9000);
    glMatrixMode(GL_MODELVIEW);

    int i = 0;
    while (i < MAX_OBJETIVOS) {
        objetivos[i]->selection_mode = true;
        i++;
    }
}


/* calcula la posicion en z donde ocurrira el choque
 * supone que nave_z = 0
 * supone que obj_z < 0
 */
int calcZ(float nave_x, float nave_y, float obj_x, float obj_y, float obj_z) {
    int result, tx, ty, tz;
    float t;
    int a,b,c;
    tx = pow((obj_x - nave_x), 2);
    ty = pow((obj_y - nave_y), 2);
    tz = pow((obj_z), 2);

    c = -(tx + ty + tz);
    b = -2*obj_z;
    a = 3;
    
  
      t= ( -b + sqrt( b^2 - 4*a*c) ) / (2*a) ;   
    result = obj_z + t*2;
   
    
    return result;
}

//procesar los hists detectados en el picking
bool processHits2(GLint hits, GLuint buffer[], int sw) {
    GLint i, j, numberOfNames, objetivo, n;
    GLuint names, *ptr, minZ, *ptrNames;

    ptr = (GLuint *) buffer;
    minZ = 0xffffffff;
    for (i = 0; i < hits; i++) {
        names = *ptr;
        ptr++;
        if (*ptr < minZ) {
            numberOfNames = names;
            minZ = *ptr;
            ptrNames = ptr + 2;
        }

        ptr += names + 2;
    }
    if (numberOfNames > 0) {
        
        ptr = ptrNames;
        for (j = 0; j < numberOfNames; j++, ptr++) {
            
            objetivo = *ptr;
        }
        
        
        //buscar objetivo con ese id
        for (n = 0; objetivos[n]->id != objetivo; ++n) {
        }
        
        if(objetivos[n]->getZ()<LIM_Z){
        int new_z = calcZ(nave->getX(), nave->getY(), objetivos[n]->getX(), objetivos[n]->getY(), objetivos[n]->getZ());
        
        //disparar al objetivo especifico
        nave->disparar(objetivos[n]->getX(), objetivos[n]->getY(), new_z);
       
        return true;
        }
    }

    return false;

    

}

//detener proceso de picking
void stopPicking() {
    bool seleccion = false;
    glMatrixMode(GL_PROJECTION);
    glPopMatrix();
    glMatrixMode(GL_MODELVIEW);
    glFlush();
    hits = glRenderMode(GL_RENDER);
    if (hits != 0) {
        seleccion = processHits2(hits, selectBuf, 0);
    }
    int i = 0;
    while (i < MAX_OBJETIVOS) {
        //colocar objetivos en modo de render de seleccion
        objetivos[i]->selection_mode = false;
        i++;
    }

    if (!seleccion) {
        nave->disparar();
    }
}

//aumenta el framerate

void fastSpeed() {
    if (speed >= 1) {
        speed--;
    }
}

//decrementa el framerate

void slowSpeed() {
    if (speed <= 5) {
        speed++;
    }
}

// indica cuanto tiempo esperar para el proximo update

int frameDelay() {
    return speeds[speed][1];
}

// indica cuantos frames por update se deben realizar

int frameSkip() {
    return speeds[speed][0];
}


//funcion auxiliar para mostrar texto por pantalla
// muestra un texto en la escena.

void mostrar_texto(string cadena, int separacion, float x, float y, float z) {
    int tmp;
    glDisable(GL_LIGHTING);
    glEnable(GL_COLOR_MATERIAL);
    for (tmp = 0; tmp < cadena.length(); tmp++) {

        glColor3f(1.0, 0.0, 0.0);
        glRasterPos3f(x + tmp*separacion, y, z);
        glutBitmapCharacter(GLUT_BITMAP_HELVETICA_18, (int) cadena[tmp]);
    }
    glDisable(GL_COLOR_MATERIAL);
    glEnable(GL_LIGHTING);
}

//funcion auxiliar para pasar int a string

string intToStr(int num) {
    stringstream out;
    out << num;
    return out.str();


}
//inicializar la nave, los anillos y los objetivos
// inicializa las variables pertientes al juego.

void init() {

    nave->setLimits(LIM_X, LIM_Y);

    anillas[0] = new Anilla(nave, NULL, LIM_X, LIM_Y);
    //cambiar la posicion de los anillos
    for (int i = 1; i < MAX_ANILLAS; i++) {
        anillas[i] = new Anilla(nave, anillas[i - 1], LIM_X, LIM_Y);
    }
    //cambiar la posicion de los objetivos
    for (int i = 0; i < MAX_OBJETIVOS; i++) {
        objetivos[i] = new Objetivo(i + 1, objetivos, nave, asteroidModel, LIM_X, LIM_Y);
        objetivos[i]->empujar(i * 200);
    }

    edificios[0] = new Edificacion(texEdif, nave, 0.0);
    edificios[0]->empujarEdificio(400);

    int off = 70;
    //colocar edificios
    for (int i = 1; i < MAX_EDIFICIOS; i++) {
        edificios[i] = new Edificacion(texEdif, nave, (edificios[i - 1]->getProfundidad() / 2) + off);
        edificios[i]->empujarEdificio(600);
        off += 70;
    }


    glMatrixMode(GL_PROJECTION);
    //Importante cargar la identidad antes de colocar la camara, asi se evitan errores de posicion cuando arranca el programa	
    glLoadIdentity();
    gluPerspective(60.0, aspectratio, 1.0, 2000.0);
    
    //inicializar sonido
    initSonido();
}

// se encarga de acondicionar la ventana para reajustarla al dimensionamiento

void cambioventana(int w, int h) {
    if (h == 0)
        h = 1;
    glViewport(0, 0, w, h);

    glMatrixMode(GL_PROJECTION);

    glLoadIdentity();

    aspectratio = (float) w / (float) h;

    ancho = w;
    alto = h;
    gluPerspective(60.0, aspectratio, 1.0, 2000.0);
}

// configura los valores de iluminacion y texturas

void ConfiguracionEscena() {
    
    //Configuracion de las luces del juego
    GLfloat diffuse[] = {0.8, 0.8, 0.8, 1.0};
    GLfloat diffuse_laser[] = {1.0, 0.1, 0.1, 1.0};
    GLfloat ambiental[] = {0.15, 0.15, 0.15, 1.0};
    GLfloat ambiental_laser[] = {0.2, 0.0, 0.0, 1.0};
    GLfloat especular[] = {1.0, 1.0, 1.0, 1.0};
    GLfloat diffuse_explosion[] = {1.0, 1.0, 1.0, 1.0};
    GLfloat atenuacion_c_laser = 0.7;
    GLfloat atenuacion_l_laser = 0.0;
    GLfloat atenuacion_q_laser = 0.05;


    //configuracion de llas diferentes luces usadas en el juego
    
    //luz detras de la nave
    glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuse);
    glLightfv(GL_LIGHT0, GL_AMBIENT, ambiental);
    glLightfv(GL_LIGHT0, GL_SPECULAR, especular);
    glLightf(GL_LIGHT0, GL_CONSTANT_ATTENUATION, 1.0);
    glLightf(GL_LIGHT0, GL_LINEAR_ATTENUATION, 1.0);
    glLightf(GL_LIGHT0, GL_QUADRATIC_ATTENUATION, 1.0);

    //luces del laser
    glLightfv(GL_LIGHT1, GL_DIFFUSE, diffuse_laser);
    glLightfv(GL_LIGHT1, GL_AMBIENT, ambiental_laser);
    glLightfv(GL_LIGHT1, GL_SPECULAR, especular);
    glLightf(GL_LIGHT1, GL_CONSTANT_ATTENUATION, atenuacion_c_laser);
    glLightf(GL_LIGHT1, GL_LINEAR_ATTENUATION, atenuacion_l_laser);
    glLightf(GL_LIGHT1, GL_QUADRATIC_ATTENUATION, atenuacion_q_laser);

    glLightfv(GL_LIGHT2, GL_DIFFUSE, diffuse_laser);
    glLightfv(GL_LIGHT2, GL_AMBIENT, ambiental_laser);
    glLightfv(GL_LIGHT2, GL_SPECULAR, especular);
    glLightf(GL_LIGHT2, GL_CONSTANT_ATTENUATION, atenuacion_c_laser);
    glLightf(GL_LIGHT2, GL_LINEAR_ATTENUATION, atenuacion_l_laser);
    glLightf(GL_LIGHT2, GL_QUADRATIC_ATTENUATION, atenuacion_q_laser);

    glLightfv(GL_LIGHT3, GL_DIFFUSE, diffuse_laser);
    glLightfv(GL_LIGHT3, GL_AMBIENT, ambiental_laser);
    glLightfv(GL_LIGHT3, GL_SPECULAR, especular);
    glLightf(GL_LIGHT3, GL_CONSTANT_ATTENUATION, atenuacion_c_laser);
    glLightf(GL_LIGHT3, GL_LINEAR_ATTENUATION, atenuacion_l_laser);
    glLightf(GL_LIGHT3, GL_QUADRATIC_ATTENUATION, atenuacion_q_laser);

    //luz de explosion
    glLightfv(GL_LIGHT4, GL_DIFFUSE, diffuse_explosion);
    glLightfv(GL_LIGHT4, GL_AMBIENT, ambiental);
    glLightfv(GL_LIGHT4, GL_SPECULAR, especular);
    glLightf(GL_LIGHT4, GL_CONSTANT_ATTENUATION, 1.0);
    glLightf(GL_LIGHT4, GL_LINEAR_ATTENUATION, 0.0);
    glLightf(GL_LIGHT4, GL_QUADRATIC_ATTENUATION, 0.0);

    glEnable(GL_LIGHT0);

    //leer texturas del fondo
    glGenTextures(1, &texturaFondo);
    glBindTexture(GL_TEXTURE_2D, texturaFondo);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

    image = glmReadPPM(fondoName, &iwidth, &iheight);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, iwidth, iheight, 0, GL_RGB, GL_UNSIGNED_BYTE, image);

}

//realiza los calculos del juego mientras no se dibuja.

void idle() {
    
    if (!paused) {
        realTime = glutGet(GLUT_ELAPSED_TIME);
        if (realTime >= virtualTime) {
            int skip = frameSkip();
            virtualTime = realTime + frameDelay();

            for (int i = 0; i < MAX_ANILLAS; i++) {
                anillas[i]->idle(skip);
            }
            for (int i = 0; i < MAX_OBJETIVOS; i++) {
                objetivos[i]->idle(skip);
            }

            for (int i = 0; i < MAX_EDIFICIOS; i++) {
                edificios[i]->idle(skip);
            }
            nave->idle(skip);
            terreno->idle(skip);
            


            glutPostRedisplay();

            if (frame > 1800) {
                if(!infinito){
                endgame = true;
                paused = true;
                mostrar_texto("FIN DE JUEGO!", 1.0, -7.0, 1, 5.0);
                mostrar_texto("(Presione 'P' para continuar)", 1.0, -14.0, -5, 5.0);

                
                nave->addScore(-nave->getScore());
                }
                frame = 0;
                fpsCount = 0;
                
            } else {
                fpsCount += skip;
                frame += skip;
            }

        }

        if (realTime > fpsCheck) {
            fps = (fps + fpsCount) / 2;
            fpsCheck = realTime + 1000;
            fpsCount = 0;
        }
    }
}

//Dibujar imagen de fondo
void dibujarFondo() {
    glDisable(GL_DEPTH_TEST);
   
    glEnable(GL_TEXTURE_2D);

    glBindTexture(GL_TEXTURE_2D, texturaFondo);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, iwidth, iheight, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);


    glMatrixMode(GL_PROJECTION);
    glPushMatrix();
    glLoadIdentity();
    
    //proyectamos de forma paralela para dibujar el fondo
    glOrtho(-500.0, 500.0, -500.0, 500, 1000, -2000);


    glMatrixMode(GL_MODELVIEW);


    glPushMatrix();
    glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, mat_ambiental);
    glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, mat_difuso);
    glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, mat_especular);
    glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, mat_emision);
    //  glMaterialfv(GL_FRONT_AND_BACK,GL_SHININESS,mat_brillo);


    glBegin(GL_QUADS);
    


    glTexCoord2f(1.0, 1.0);
    glVertex3f(-500.0, -240.0, 00.0);

    glTexCoord2f(0.0, 1.0);
    glVertex3f(500.0, -240.0, 00.0);


    glTexCoord2f(0.0, 0.0);
    glVertex3f(500.0, 500.0, 00.0);


    glTexCoord2f(1.0, 0.0);
    glVertex3f(-500.0, 500.0, 00.0);


    glEnd();


    glPopMatrix();

    glMatrixMode(GL_PROJECTION);
    glPopMatrix();

    glMatrixMode(GL_MODELVIEW);


    glDisable(GL_TEXTURE_2D);
    

    glEnable(GL_DEPTH_TEST);



}

//dibuja la escena

void display() {




    glMatrixMode(GL_MODELVIEW);
    //Importante cargar la identidad antes de colocar la camara, asi se evitan errores de posicion cuando arranca el programa	
    glLoadIdentity();
    if (!paused) {
        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        dibujarFondo();
        
        terreno->dibujar();
        // dibuja cada uno de los objetos en la escena
        
        nave->dibujar();
        for (int i = 0; i < MAX_EDIFICIOS; i++) {
            edificios[i]->dibujar();
        }

        
        int puntaje = nave->getScore();
        string pre = "SCORE: ";

        if (puntaje < 0) {
            puntaje = -puntaje;
            pre = "SCORE: -";
        }
        //presentar puntaje
        string s = intToStr(puntaje);
        mostrar_texto(pre + s, 1.0, 10.0, 10.0, 5.0);

        //presentar framerate
        pre = "FPS " + intToStr(fps);
        mostrar_texto(pre, 1.0, -4.0, -10.0, 5.0);

        //presentar tiempo
        if(!infinito){
        pre = "TIME: " + intToStr((int) frame / 30) + " Sec";
        mostrar_texto(pre, 1, -15.0, 10.0, 5.0);
        }
        //dibujar objetivos
        for (int i = 0; i < MAX_OBJETIVOS; i++) {
            objetivos[i]->dibujar();
        }
        //dibujar anillos
        for (int i = 0; i < MAX_ANILLAS; i++) {
            anillas[i]->dibujar();
        }

        


    }
    glutSwapBuffers();

}

//captura las teclas presionadas para mover la camara y posicion de los objetos

void teclado(unsigned char t, int x, int y) {

    if (t == 'p') {
        paused = !paused;
        mostrar_texto("PAUSA", 1.0, -2.0, 0, 5.0);

    } else {

        if (!paused) {

            switch (t) {
                    break;
                case 'w': nave->moverArriba();
                    break;
                case 's': nave->moverAbajo();
                    break;
                case 'a': nave->moverIzq();
                    break;
                case 'd': nave->moverDer();
                    break;
                case '+': fastSpeed();
                    break;
                case '-': slowSpeed();
                    break;
                case 'o': if (infinito) {
                        infinito = false;
                        frame = 0;
                        fpsCount = 0;
                    } else {
                        infinito = true;
                    }

            }
        }

    }
    glutPostRedisplay();
}



//captura cuando se le hace click a la pantalla

void click(int button, int state, int x, int y) {
    if (!paused) {
        if (button != GLUT_LEFT_BUTTON || state != GLUT_DOWN)
            return;

        startPicking(x, y);
        //  display();
        int n = 0;
        while (n < MAX_OBJETIVOS) {
            objetivos[n]->dibujar();
            n++;
        }
        stopPicking();


    }
}




//metodo principal del juego

int main(int argc, char** argv) {

    glutInit(&argc, argv);
    glutInitWindowSize(800, 600);
    glutInitWindowPosition(10, 50);
    glutCreateWindow("SpaceTroupial");
    glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE);

    glEnable(GL_DEPTH_TEST); //activar iluminacion y profundidad
   
    
    glEnable(GL_LIGHTING);
    
    ConfiguracionEscena();
    alutInit(&argc, argv) ; 
    init();

    glutDisplayFunc(display);

    glutReshapeFunc(cambioventana);
    glutKeyboardFunc(teclado);
    glutMouseFunc(click);
    glutIdleFunc(idle);

    glutMainLoop();

    return 0;
}
